import Raphael from "raphael";
import dagre from "@dagrejs/dagre";
import svgPanZoom from 'svg-pan-zoom';
import { Canvg, presets } from "canvg";
import config from "./config";

const preset = presets.offscreen();

export class DagreMap {
    constructor(containerId="",  options = {
        rankdir: 'TB',
        align: undefined, 
        nodesep: 50,
        edgesep: 10,
        ranksep: 50,
        marginx: 0,
        marginy: 0,
        ranker: 'network-simplex',
        directed: true,
        multigraph: false,
        compound: false,
        nodes: [],
        links: [],
        edge:{
            fill: 'tomato',
            connector: 'curve'
        },
        viewPort: {
            zoom: 0.6
        }
    }) {
        this.wrapperId = containerId
        this.config = Object.assign(config, {...options})
        this.paper = {}
        this.graph = {}
        this.coordinateNodes = []
        this.coordinateEdges = []
        this.panZoomTiger = {}
        this.renderNodes = queueNodes.bind(this)
        this.renderEdges = queueEdges.bind(this)
        this.transferLinkToStr = transferLinkToStr.bind(this)
        this.initGraph()
        this.initPaper()
        this.initSvgPanZoom()        
    }

    initGraph() {
        const containerDom = document.querySelector(`${this.wrapperId}`);
        const { clientWidth, clientHeight } = containerDom;
        this.graph = new dagre.graphlib.Graph({
            directed: this.config.directed,
            compound: this.config.compound,
            multigraph: this.config.multigraph
        })
        this.graph.setGraph({
            rankdir: this.config.rankdir, // 节点布局方式：RL,LR,TB,BT
            nodesep: this.config.nodesep, // 节点之间的垂直间距
            ranksep: this.config.ranksep, // 节点之间的水平间距
            // align: "UL", // 根节点对齐方式 DL,DR,UL,UR;这个参数会影响树布局方式，无特殊情况，建议关闭
            ranker: this.config.ranker,
            width: clientWidth,
            height: clientHeight
        })
        this.graph.setDefaultEdgeLabel(function () { return {} })

        // 配置节点
        this.config.nodes.map(el => this.graph.setNode(el.id, el.node))
        // 配置边
        this.config.links.map(el => this.graph.setEdge(el.sourceId, el.targetId))

        // 调用dagre.layout接口进行计算节点坐标和边坐标
        this.layout()
    }

    layout () {
        this.coordinateNodes = []
        this.coordinateEdges = []

        // function 内部不能访问外部this，需要重新赋值
        dagre.layout(this.graph)

        // 获取带有坐标的节点
        this.graph.nodes().map((v) => this.coordinateNodes.push(this.graph.node(v)));
        // 获取带有坐标的边
        this.graph.edges().map((v) => this.coordinateEdges.push(this.graph.edge(v)));

    }

    update (rankdir) {
        this.destroy()
        this.config.rankdir = rankdir
        this.initGraph()
        this.initPaper()
        this.initSvgPanZoom()
    }

    initPaper () {
        const containerDom = document.querySelector(`${this.wrapperId}`);
        const { clientWidth, clientHeight } = containerDom;
        
        // 创建SVG画布，每次都会新增到容器顶部
        this.paper = new Raphael(containerDom, clientWidth, clientHeight)
        // DagreJS图形的宽度和高度
        var graphWidth = this.graph.graph().width;
        var graphHeight = this.graph.graph().height;

        // 计算位移量
        var dx = (clientWidth - graphWidth) / 2;
        var dy = (clientHeight - graphHeight) / 2;
        // 设置viewBox保证内容完全展示，辅助initSvgPanZoom控制视图，避免切换布局后出现大图再变小图问题
        const svg = this.paper.setViewBox(-dx, -dy, clientWidth, clientHeight)
        // 渲染节点和边
        this.initRender()
    }

    initRender () {
        this.renderEdges()
        this.renderNodes()
    }

    initSvgPanZoom() {
        const svgElement = document.querySelector(`${this.wrapperId} > svg`)
        this.panZoomTiger = svgPanZoom(svgElement, {
            viewportSelector: `${this.wrapperId} > svg`,
            panEnabled: true, // 启用拖拽
            zoomEnabled: true, // 开启缩放
            controlIconsEnabled: true, // 开启图标控制，点击图标缩放
            dblClickZoomEnabled: false, // 禁止双击鼠标进行缩放视图
            mouseWheelZoomEnabled: true, // 开启鼠标滚轮缩放视图
            zoomScaleSensitivity: 0.2, // 变焦灵敏度 (Default 0.2)
            minZoom: 0.5, // 最小缩放级别 (Default 0.5)
            maxZoom: 10, // 最大缩放级别  (Default 10)
            fit: true, // 启用或禁用 SVG 中的视口适配
            center: true, // 在 SVG 中启用或禁用视口居中
            refreshRate: 10,
            beforeZoom: function (oldScale, newScale) {
                // console.log(oldScale, newScale, 7777);
            }
        })
        // 默认以1倍查看视图，可以放大当前视图倍数，以获得更加的视觉效果
        this.panZoomTiger.zoom(this.config.viewPort.zoom)
        // 配置最小倍数
        // console.log(this.viewPort, 'initSvgPanZoom');
    }

    exportPng () {
        const container = document.querySelector(this.wrapperId)
        const svgDom = container.querySelector('svg')
        svgDom.removeAttributeNS(null, 'xmlns:xlink')
        svgDom.removeAttributeNS(null, 'xmlns:ev')
        const { width:svgWidth, height:svgHeight } = svgDom.getBoundingClientRect()
        // 将svg转换成图片可以预览
        this.svgConvertToPng({
            width: svgWidth,
            height: svgHeight,
            svg: svgDom.outerHTML
        }).then(pngUrl => {
            this.imgURL = pngUrl
        })
    }

    async svgConvertToPng(data) {
        const { width, height, svg } = data;
        const canvas = new OffscreenCanvas(width, height);
        const ctx = canvas.getContext("2d");
        const v = await Canvg.from(ctx, svg, preset);
    
        // Render only first frame, ignoring animations and mouse.
        await v.render();
    
        const blob = await canvas.convertToBlob();
        const pngUrl = URL.createObjectURL(blob);
        const a = document.createElement('a')
        a.style.display = 'none'
        document.body.append(a)
        a.href = pngUrl
        a.download = `${timeStamp()}.png`
        a.click()
        window.URL.revokeObjectURL(a.href)
        document.body.removeChild(a)
        return pngUrl;
    }

    destroy () {
        this.panZoomTiger.destroy()
        // 清空画布
        this.paper.clear()
        // 移除画布
        this.paper.remove()
    }
}

function queueNodes(params) {
    let i = 0
    while (i < this.coordinateNodes.length) {
        // 然而，DagreJS对位置的计算则略有不同，其x和y是基于元素的中心点进行的。
        // 需要转换元素x和y坐标才能对齐连线
        const rect = this.paper.rect(this.coordinateNodes[i].x - this.coordinateNodes[i].width / 2, this.coordinateNodes[i].y - this.coordinateNodes[i].height / 2, this.coordinateNodes[i].width, this.coordinateNodes[i].height, 5);
        rect.attr({ fill: Raphael.getColor(), stroke: 'none'})
        const rectBox = rect.getBBox()
        const { cx, cy, x:Rx, y:Ry, width, height }  = rectBox
        let textX = Rx + width / 2, textY = Ry + height / 2;
        const text = this.paper.text(textX, textY, this.coordinateNodes[i].label)
        text.attr({'font-size': 18, 'font-weight': 600, fill: '#fff'})
        i++;
    }
}

function queueEdges(params) {
    // 获取连接线数据
    // const newLinks = linkConnector(this.config.edge.connector, this.coordinateEdges)
    const newLinks = this.transferLinkToStr()
    let j = 0;
    while (j < newLinks.length) {
        const path = this.paper.path(newLinks[j])
            path.attr('stroke', Raphael.getColor())
            path.attr('stroke-width', 3)
        j++;
    }
}

/**
 * 绘制线条模式
 * 当前仅支持 curve曲线  straight直线
 * 
 * connector 曲线名称 curve || straight
 * 
 * 注意：箭头函数会让绑定的this失效
 */
export function transferLinkToStr () {
    let newLineSet = []
    if (this.config.edge.connector == void 0) return this.coordinateEdges
    if (!Array.isArray(this.coordinateEdges)) {
        new Error('The connection line must be an array of objects')
    }
    if (this.coordinateEdges.length == 0) return []
    
    let i = 0;
    while (i < this.coordinateEdges.length) {
        const { points } = this.coordinateEdges[i];
        let _D = '';
        for(let k = 0; k < points.length; k++) {
            // straight 
            if (this.config.edge.connector === 'straight') {
                if (k === 0) {
                    _D += `M ${points[k].x} ${points[k].y} `
                } else {
                    _D += `L ${points[k].x} ${points[k].y} `
                }
            }
            // curve 
            if (this.config.edge.connector === 'curve') {
                if (k === 0) {
                    _D += `M ${points[k].x} ${points[k].y} `
                } else if (k === 1){
                    _D += `Q ${points[k].x} ${points[k].y} `
                } else {
                    _D += `${points[k].x} ${points[k].y}`
                }
            }  
        }
        newLineSet.push(_D)
        i++;
    }
    
    return newLineSet
}

export const timeStamp = () => {
    var date = new Date()
    var y = date.getFullYear()
    var m = date.getMonth()+1
    var d = date.getDate()
    var hour = date.getHours()
    var minutes = date.getMinutes()
    var seconds = date.getSeconds()
    return `${y}${m}${d}_${hour}${minutes}${seconds}`
}